Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solana support #15428

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft

Solana support #15428

wants to merge 1 commit into from

Conversation

pablolagreca
Copy link
Collaborator

@pablolagreca pablolagreca commented Nov 26, 2024

Basic refactor to start introducing Solana tooling implementation.

Changes:

  • CCIPOnChainState: refactor to have EVM state and Solana state fields. Solana state is empty until implemented
  • Chain: refactor to have EVM chain operations and Solana chain operations. Solana operations are empty until implemented.
  • Chain: Add Family field so we can delegate to the Solana or the EVM operations
  • Method extraction for chain family specific logic. Adding EVM logic into an EVM specific file.

// Chain represents an EVM chain.
type Chain struct {
// Selectors used as canonical chain identifier.
Family string
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Family field. We would switch logic based on the Chain family.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking (see TODO on Environment) it'd be like this

type Environment struct {
	Name              string
	Logger            logger.Logger
	ExistingAddresses AddressBook
	EVMChains       map[uint64]EVMChain
	SolChains  	 map[uint64]SolanaChain
	NodeIDs           []string
	Offchain          OffchainClient
}

just to avoid the partially populated structs and allow for easy top level family "switch". Imagining most of the time the changesets will be scoped to a family and just operate purely on the EVMChains or SolChains, so easily looking up all the chains in a family makes it easier. Example:

type _ deployment.Changeset[DeploySolanaConfig] = DeploySolana

func DeploySolana(e deployment.Environment, cfg DeploySolanaConfig) (deployment.ChangesetOutput, error) {
	// validate config
	ab := newAddressBook()
	if err := deployContractCCIPRampSolana(ab, e.SolChains[cfg.ChainSelector], cfg.ContractAConfig); err != nil {	
		//...
	}
	return deployment.ChangesetOutput{AddressBook: ab}, nil 
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cgruber curious wyt

Copy link
Collaborator Author

@pablolagreca pablolagreca Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are already common aspects for every chain, like selector or family that it would be good to have under the same struct for code reuse. Also functions that depend on Chain but we may not know in advance which chain family it will interact with it will have to receive the whole Environment struct. Additionally the number of non-EVM chains will grow next year to 8 non-EVM chains so that would be 8 fields more within Environment. I would prefer to isolate that complexity within the Chain struct.

Another thing, maybe minor, is that this proposal wouldn't allow in any way to have two different Chain for the same chain selector.

I also think that right now it's not clear that if there are pieces that we can hide behind CR/CW and simplify the code base of chainlink deployments. I'm not sure we really need to use chain specific bindings. That's TBD. I already seen that, for instance, OCR3Config it's the same (or can be adapted through CR/CW config) for Solana and for EVM.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the code is hard for me to understand with this pattern

eg in keystone logic we have things now like
registryChain.EVMChain

this doesn't make sense - there is no such thing a non-EVM registry chain

and i don't understand how i'm supposed to use it.
is it ever valid to have non-empty EVM & Solana? i think no. so at every call site I have to remember which kind of thing I'm dealing with rather than instantiating the correct thing once.

i don't understand the comment above out CR/CW - those are chain agnostic but this change is adding more explicit chains.

so discounting the CR/CW discussion, then the if the concern is reusing common config fields, we can embed them in the structs ~

type CommonStuff struct {
Selector
Family
...
}

type EVMChain struct {
CommonStuff
EVMThingA
EVMThingB
}

type SolanaChain struct {
CommonStuff
SolanaThingC
} 

I would expect the different families to be distinguished in the environment along the lines of what @connorwstein said above

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, we should not be using strings for family here. We need an enum (or typed set of constants).

Alternatively could we actually use the type of the struct itself? like

struct ChainFamily[CS any] {}

var (
    SOLANA_FAMILY  = ChainFamily[SolanaState]{}
    EVM_FAMILY     = ChainFamily[EVMState]{}
    // ... other families
)

Then our state could include

type Environment struct {
    chains map[ChainFamily[any]]map[uint64]any
}

func (gs *Environment) GetChainState[CS any](family ChainFamily[CS], chainId uint64) (CS, error) {
    var zeroState CS
    if familyMap, ok := gs.chains[family]; ok {
        if state, ok := familyMap[chainId]; ok {
            return state.(STATE), nil // Type assertion to return the correct type
        }
        return zeroState, fmt.Errorf("chain with ID %d not found in family", chainId)
    }
    return zeroState, fmt.Errorf("family not found")
}

Then callers just call:

state, err := globalState.GetChainState(SOLANA_FAMILY, 12345) 
if err != nil {
    // Handle error
}
foo := state.SomeSolanaSpecificValue

Then there's no different way to access any given chain. You do have to tell it what family. We can have a simple utility to get that from the selector - map a selector chain family to a given state struct somewhere. But Reference chains are just chains, per @krehermann's concern, the structure of the state is per-family, so we can have strongly typed access to the structure, etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at some of the usages of chain family, it could be a two-step thing too. GetChainFamily(SOLANA_FAMILY).Chains(selectorId). But the same principle works here, it's just that I misunderstood the structure of the chain family in my first example.

registryChain would just have the family we use for the registry chain, it wouldn't be chain-neutral like this. etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a perfect adaptation for the current usage. I'm trying to roughly sketch out (a) something that we can access in a strongly typed way with generics, and (b) that allows us to access things in a more uniform way (so we dont' have a pattern of if sol { return chains.SolanaStuff } else { return chains.EvmStuff } all over. Instead, it's "get the family, then look stuff up with reference to the family to get the strong typing." The same logic is encoded, but not in call-site if-blocks.

@pablolagreca pablolagreca force-pushed the solana-support branch 5 times, most recently from d2f229c to 28cf95a Compare December 4, 2024 12:31
@pablolagreca pablolagreca force-pushed the solana-support branch 4 times, most recently from 50e2fe6 to b5e8768 Compare December 4, 2024 16:04
@pablolagreca pablolagreca marked this pull request as ready for review December 4, 2024 18:45
@pablolagreca pablolagreca requested review from a team as code owners December 4, 2024 18:45
@@ -233,12 +233,20 @@ func (c CCIPChainState) GenerateView() (view.ChainView, error) {
// Offchain state always derivable from a list of nodeIds.
// Note can translate this into Go struct needed for MCMS/Docs/UI.
type CCIPOnChainState struct {
SolanaState SolanaCCIPOnChainState
EVMState EVMCCIPOnChainState
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine as far as it goes, but it does render the logic pretty clearly chain-specific, because it has to dereference a specific field for that family. This ends up with a lot of if/then logic, in the end. Maybe that's unavoidable, insofar as it is chain specific structures in the state anyway. But this feels like a case for generics. But go's generics may not be flexible enough for a good solution here. For now, this is fine, and we can look at the if/then logic that proceeds, and re-evaluate if we want to hide this behind a more robust structure.

// Chain represents an EVM chain.
type Chain struct {
// Selectors used as canonical chain identifier.
Family string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, we should not be using strings for family here. We need an enum (or typed set of constants).

Alternatively could we actually use the type of the struct itself? like

struct ChainFamily[CS any] {}

var (
    SOLANA_FAMILY  = ChainFamily[SolanaState]{}
    EVM_FAMILY     = ChainFamily[EVMState]{}
    // ... other families
)

Then our state could include

type Environment struct {
    chains map[ChainFamily[any]]map[uint64]any
}

func (gs *Environment) GetChainState[CS any](family ChainFamily[CS], chainId uint64) (CS, error) {
    var zeroState CS
    if familyMap, ok := gs.chains[family]; ok {
        if state, ok := familyMap[chainId]; ok {
            return state.(STATE), nil // Type assertion to return the correct type
        }
        return zeroState, fmt.Errorf("chain with ID %d not found in family", chainId)
    }
    return zeroState, fmt.Errorf("family not found")
}

Then callers just call:

state, err := globalState.GetChainState(SOLANA_FAMILY, 12345) 
if err != nil {
    // Handle error
}
foo := state.SomeSolanaSpecificValue

Then there's no different way to access any given chain. You do have to tell it what family. We can have a simple utility to get that from the selector - map a selector chain family to a given state struct somewhere. But Reference chains are just chains, per @krehermann's concern, the structure of the state is per-family, so we can have strongly typed access to the structure, etc.

// Chain represents an EVM chain.
type Chain struct {
// Selectors used as canonical chain identifier.
Family string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at some of the usages of chain family, it could be a two-step thing too. GetChainFamily(SOLANA_FAMILY).Chains(selectorId). But the same principle works here, it's just that I misunderstood the structure of the chain family in my first example.

registryChain would just have the family we use for the registry chain, it wouldn't be chain-neutral like this. etc.

// Chain represents an EVM chain.
type Chain struct {
// Selectors used as canonical chain identifier.
Family string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a perfect adaptation for the current usage. I'm trying to roughly sketch out (a) something that we can access in a strongly typed way with generics, and (b) that allows us to access things in a more uniform way (so we dont' have a pattern of if sol { return chains.SolanaStuff } else { return chains.EvmStuff } all over. Instead, it's "get the family, then look stuff up with reference to the family to get the strong typing." The same logic is encoded, but not in call-site if-blocks.

@pablolagreca pablolagreca marked this pull request as draft December 9, 2024 18:57
@smartcontractkit smartcontractkit deleted a comment from github-actions bot Dec 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants